/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Validate } from '../helper/Validate';
import { Reader } from '../io/Reader';
import { TokeniserState } from './TokeniserState';
import FaultLogger from '@ohos.faultLogger';
import ArrayList from '@ohos.util.ArrayList';
import List from '@ohos.util.List';
import { ArrayUtils } from '../util/ArrayUtils';
import hilog from '@ohos.hilog';
import { StringReader } from '../io/StringReader';
import constants from '../util/Constants';

export class CharacterReader {
  private static readonly _maxStringCacheLen = 12
  // visible for testing
  static readonly maxBufferLen = 1024 * 32;
  // visible for testing
  static readonly readAheadLimit = Math.floor(CharacterReader.maxBufferLen * 0.75)
  // the minimum mark length supported. No HTML entities can be larger than this.
  private static readonly _minReadAheadLen = 1024
  private _charBuf: string[]
  private _reader: Reader;
  private _bufLength: number = 0
  private _bufSplitPoint: number = 0
  private _bufPos: number = 0
  private _readerPos: number = 0
  private _bufMark = -1
  private static readonly _stringCacheSize = 512
  // holds reused strings in this doc, to lessen garbage
  private _stringCache: string[] = []
  private _newlinePositions: number[] = null
  private _lineNumberOffset = 1
  private readonly _charBufLen:number

  constructor(input:string);
  constructor(input:Reader);
  constructor(input: string, sz: number)
  constructor(input: Reader, sz: number)
  constructor(input: Reader|string, sz: number = 1024 * 32) {
    let newStr:Reader
    if (typeof input === 'string') {
      newStr = new StringReader(input)
      sz = input.length
    } else if (input instanceof Reader){
      newStr = input
    }
    Validate.notNull(newStr)
    Validate.isTrue(newStr.markSupported())
    this._reader = newStr
    this._charBuf = []
    this._charBufLen = Math.min(sz,CharacterReader.maxBufferLen)
    this.bufferUp()
  }

  close() {
    if (this._reader === undefined || this._reader === null) {
      return
    }
    try {
      this._reader.close()
    } catch (e) {
    }
    finally {
      this._reader = null
      this._charBuf = null
      this._stringCache = null
    }
  }

  // if the underlying stream has been completely read, no value in further buffering
  private _readFully: boolean = false

  private bufferUp() {
    if (this._readFully || this._bufPos < this._bufSplitPoint) {
      return
    }
    let pos: number
    let offset: number
    if (this._bufMark != -1) {
      pos = this._bufMark
      offset = this._bufPos - this._bufMark
    } else {
      pos = this._bufPos
      offset = 0
    }
    try {
      let skipped = this._reader.skip(pos)
      this._reader.mark(CharacterReader.maxBufferLen)
      let read = 0
      while (read <= CharacterReader._minReadAheadLen) {
        let thisRead = this._reader.read1(this._charBuf, this._charBufLen, read, this._charBufLen - read)
        if (thisRead === -1) {
          this._readFully = true
        }
        if (thisRead <= 0) {
          break
        }
        read += thisRead
      }
      this._reader.reset()
      if (read > 0) {
        // Previously asserted that there is room in buf to skip, so this will be a WTF
        Validate.isTrue(skipped === pos)
        this._bufLength = read
        this._readerPos += pos
        this._bufPos = offset
        if (this._bufMark != -1) {
          this._bufMark = 0
        }
        this._bufSplitPoint = Math.min(this._bufLength, CharacterReader.readAheadLimit)
      }
    } catch (error) {
      throw new Error(JSON.stringify(error))
    }

    // if enabled, we index newline positions for line number tracking
    this.scanBufferForNewlines();
    // cache for last containsIgnoreCase(seq)
    this._lastIcSeq = null
  }

  /**
   * Gets the current cursor position in the content.
   * @return current position
   */
  pos(): number {
    return this._readerPos + this._bufPos;
  }

  /** Tests if the buffer has been fully read. */
  readFully(): boolean {
    return this._readFully
  }
  /**
   Enables or disables line number tracking. By default, will be <b>off</b>.Tracking line numbers improves the
   legibility of parser error messages, for example. Tracking should be enabled before any content is read to be of
   use.

   @param track set tracking on|off
   @since 1.14.3
   */
  public trackNewlines(track: boolean) {
    if (track && this._newlinePositions === null) {
      this._newlinePositions = []
      // first pass when enabled; subsequently called during bufferUp
      this.scanBufferForNewlines();
    } else if (!track) {
      this._newlinePositions = null
    }
  }

  /**
   Check if the tracking of newlines is enabled.
   @return the current newline tracking state
   @since 1.14.3
   */
  public isTrackNewlines(): boolean {
    return this._newlinePositions !== undefined && this._newlinePositions !== null
  }

  /**
   Get the current line number (that the reader has consumed to). Starts at line #1.
   @return the current line number, or 1 if line tracking is not enabled.
   @since 1.14.3
   @see #trackNewlines(boolean)
   */
  public lineNumber(): number {
    return this.lineNumber1(this.pos())
  }

  lineNumber1(pos: number): number {
    // note that this impl needs to be called before the next buffer up or line numberoffset will be wrong. if that
    // causes issues, can remove the reset of newlinepositions during buffer, at the cost of a larger tracking array
    if (!this.isTrackNewlines()) {
      return 1
    }
    let i = this.lineNumIndex(pos)
    if (i === -1) {
      return this._lineNumberOffset
    }
    return i + this._lineNumberOffset + 1
  }

  /**
   Get the current column number (that the reader has consumed to). Starts at column #1.
   @return the current column number
   @since 1.14.3
   @see #trackNewlines(boolean)
   */
  public columnNumber(): number {
    return this.columnNumber1(this.pos())
  }

  columnNumber1(pos: number): number {
    if (!this.isTrackNewlines()) {
      return pos + 1
    }
    let i:number = this.lineNumIndex(pos)
    if (i === -1) {
      return pos + 1
    }
    let result = pos - this._newlinePositions[i] + 1
    hilog.info(0x0000, 'testTag', '%{public}s', `columnNumber :${this._newlinePositions[0]},${this._newlinePositions[1]},${this._newlinePositions[2]}`);
    return result
  }

  /**
   Get a formatted string representing the current line and cursor positions. E.g. <code>5:10</code> indicating line
   number 5 and column number 10.
   @return line:col position
   @since 1.14.3
   @see #trackNewlines(boolean)
   */
  cursorPos(): string {
    return `${this.lineNumber()}:${this.columnNumber()}`
  }

  private lineNumIndex(pos: number): number {
    if (!this.isTrackNewlines()) {
      return 0
    }
    let i = ArrayUtils.binarySearch(this._newlinePositions, pos)
    hilog.info(0x0000, 'testTag', '%{public}s', `lineNumIndex ${i}`);
    if (i < -1) {
      i = Math.abs(i) - 2
    }
    return i
  }

  /**
   Scans the buffer for newline position, and tracks their location in newlinePositions.
   */
  private scanBufferForNewlines() {
    if (!this.isTrackNewlines()) {
      return
    }
    if (this._newlinePositions.length > 0) {
      // work out the line number that we have read up to (as we have likely scanned past this point)
      let index = this.lineNumIndex(this._readerPos)
      if (index === -1) {
        index = 0
      }
      let linePos = this._newlinePositions[index]
      // the num lines we've read up to
      this._lineNumberOffset += index
      this._newlinePositions.length = 0
      this._newlinePositions.push(linePos)
    }
    for (let i = this._bufPos;i < this._bufLength; i++) {
      if (this._charBuf[i] === '\n') {
        this._newlinePositions.push(1 + this._readerPos + i)
      }
    }
    let len = this._newlinePositions.length
    for (let i = 0;i < len; i++) {
      hilog.info(0x0000, 'testTag', '%{public}s', `scanBufferForNewlines:${this._newlinePositions.length}  ${this._newlinePositions[i]} `);
    }
    hilog.info(0x0000, 'testTag', '%{public}s', `scanBufferForNewlines end len:${this._bufLength}`);
  }

  /**
   * Tests if all the content has been read.
   * @return true if nothing left to read.
   */
  public isEmpty(): boolean {
    this.bufferUp();
    return this._bufPos >= this._bufLength;
  }

  private isEmptyNoBufferUp(): boolean {
    return this._bufPos >= this._bufLength
  }

  /**
   * Get the char at the current position.
   * @return char
   */
  public current(): string {
    this.bufferUp();
    return this.isEmptyNoBufferUp() ? constants.eof : this._charBuf[this._bufPos];
  }

  consume(): string {
    this.bufferUp();
    let val = this.isEmptyNoBufferUp() ? constants.eof : this._charBuf[this._bufPos];
    this._bufPos++;
    return val;
  }

  unconsume() {
    if (this._bufPos < 1) {
      throw new Error("No buffer left to unconsume");
    }
    this._bufPos--;
  }

  /**
   * Moves the current position by one.
   */
  public advance() {
    this._bufPos++;
  }

  mark() {
    // make sure there is enough look ahead capacity
    if (this._bufLength - this._bufPos < CharacterReader._minReadAheadLen) {
      this._bufSplitPoint = 0;
    }
    this.bufferUp();
    this._bufMark = this._bufPos;
  }

  unmark() {
    this._bufMark = -1
  }

  rewindToMark() {
    if (this._bufMark === -1) {
      throw new Error("Mark invalid")
    }
    this._bufPos = this._bufMark
    this.unmark()
  }

  /**
   * Returns the number of characters between the current position and the next instance of the input char
   * @param c scan target
   * @return offset between current position and next instance of target. -1 if not found.
   */
  nextIndexOfInt(c: number): number {
    // doesn't handle scanning for surrogates
    this.bufferUp();
    for (let i = this._bufPos; i < this._bufLength; i++) {
      if (c === this._charBuf[i].charCodeAt(0)) {
        return i - this._bufPos
      }
    }
    return -1
  }
  /**
   * Returns the number of characters between the current position and the next instance of the input sequence
   *
   * @param seq scan target
   * @return offset between current position and next instance of target. -1 if not found.
   */
  nextIndexOf(seq: string): number {
    this.bufferUp()
    // doesn't handle scanning for surrogates
    let startChar = seq[0];
    for (let offset = this._bufPos; offset < this._bufLength; offset++) {
      // scan to first instance of startchar:
      if (startChar !== this._charBuf[offset]) {
        while (++offset < this._bufLength && startChar !== this._charBuf[offset]) { /* empty */
        }
      }
      let i = offset + 1;
      let last = i + seq.length - 1;
      if (offset < this._bufLength && last <= this._bufLength) {
        for (let j = 1; i < last && seq[j] === this._charBuf[i]; i++, j++) { /* empty */
        }
        if (i === last) {
          // found full sequence
          return offset - this._bufPos;
        }
      }
    }
    return -1;
  }

  /**
   * Reads characters up to the specific char.
   * @param c the delimiter
   * @return the chars read
   */
  public consumeTo(c: string | number): string {
    if (typeof c === 'string') {
      let offset = this.nextIndexOf(c)
      if (offset !== -1) {
        let consumed = CharacterReader.cacheString(this._charBuf, this._stringCache, this._bufPos, offset);
        this._bufPos += offset
        return consumed
      } else if (this._bufLength - this._bufPos < c.length) {
        // nextIndexOf() did a bufferUp(), so if the buffer is shorter than the search string, we must be at EOF
        return this.consumeToEnd();
      } else {
        // the string we're looking for may be straddling a buffer boundary, so keep (length - 1) characters
        // unread in case they contain the beginning of the search string
        let endPos = this._bufLength - c.length + 1
        let consumed = CharacterReader.cacheString(this._charBuf, this._stringCache, this._bufPos, endPos - this._bufPos)
        this._bufPos = endPos
        return consumed
      }
    } else if (typeof c === 'number') {
      let offset = this.nextIndexOfInt(c)
      if (offset !== -1) {
        let consumed = CharacterReader.cacheString(this._charBuf, this._stringCache, this._bufPos, offset)
        this._bufPos += offset
        return consumed
      } else {
        return this.consumeToEnd()
      }
    }
  }

  /**
   * Read characters until the first of any delimiters is found.
   * @param chars delimiters to scan for
   * @return characters read up to the matched delimiter.
   */
  public consumeToAny(...chars: string[]): string {
    this.bufferUp()
    let pos = this._bufPos
    let start = pos
    let remaining = this._bufLength
    let val = this._charBuf
    let charLen = chars.length
    let i: number
    OUTER: while (pos < remaining) {
      for (i = 0; i < charLen; i++) {
        if (val[pos] === chars[i])
          break OUTER;
      }
      pos++;
    }
    this._bufPos = pos
    return pos > start ? CharacterReader.cacheString(this._charBuf, this._stringCache, start, pos - start) : ""
  }

  consumeToAnySorted(chars: string[]): string {
    this.bufferUp()
    let pos = this._bufPos
    let start = pos
    let remaining = this._bufLength
    let val = this._charBuf
    while (pos < remaining) {
      if (ArrayUtils.binarySearch(chars, val[pos]) >= 0) {
        break;
      }
      pos++;
    }
    this._bufPos = pos
    return this._bufPos > start ? CharacterReader.cacheString(this._charBuf, this._stringCache, start, pos - start) : ""
  }

  consumeData(): string {
    // &, <, null
    //bufferUp(); // no need to bufferUp, just called consume()
    let pos = this._bufPos
    let start = pos
    let remaining = this._bufLength
    let val = this._charBuf
    OUTER:while (pos < remaining) {
      switch (val[pos]) {
        case '&':
        case '<':
        case constants.nullChar:
          break OUTER;
        default:
          pos++;
      }
    }
    this._bufPos = pos
    return pos > start ? CharacterReader.cacheString(this._charBuf, this._stringCache, start, pos - start) : ""
  }

  consumeAttributeQuoted(single: boolean): string {
    // null, " or ', &
    //bufferUp(); // no need to bufferUp, just called consume()
    let pos = this._bufPos
    let start = pos
    let remaining = this._bufLength
    let val = this._charBuf
    OUTER:while (pos < remaining) {
      switch (val[pos]) {
        case '&':
        case constants.nullChar:
          break OUTER
        case '\'':
          if (single) {
            break OUTER
          }
        case '"':
          if (!single) {
            break OUTER
          }
        default:
          pos++;
      }
    }
    this._bufPos = pos
    return pos > start ? CharacterReader.cacheString(this._charBuf, this._stringCache, start, pos - start) : ""
  }

  consumeRawData(): string {
    // <, null
    //bufferUp(); // no need to bufferUp, just called consume()
    let pos = this._bufPos
    let start = pos
    let remaining = this._bufLength
    let val = this._charBuf
    OUTER:while (pos < remaining) {
      switch (val[pos]) {
        case '<':
        case constants.nullChar:
          break OUTER;
        default:
          pos++;
      }
    }
    this._bufPos = pos
    return pos > start ? CharacterReader.cacheString(this._charBuf, this._stringCache, start, pos - start) : ""
  }

  consumeTagName(): string {
    // '\t', '\n', '\r', '\f', ' ', '/', '>', nullChar
    // NOTE: out of spec, added '<' to fix common author bugs
    this.bufferUp()
    let pos = this._bufPos
    let start = pos
    let remaining = this._bufLength
    let val = this._charBuf
    OUTER:while (pos < remaining) {
      switch (val[pos]) {
        case '\t':
        case '\n':
        case '\r':
        case '\f':
        case ' ':
        case '/':
        case '>':
        case '<':
        case constants.nullChar:
          break OUTER;
      }
      pos++
    }
    this._bufPos = pos
    return pos > start ? CharacterReader.cacheString(this._charBuf, this._stringCache, start, pos - start) : ""
  }

  consumeToEnd(): string {
    this.bufferUp();
    let data = CharacterReader.cacheString(this._charBuf, this._stringCache, this._bufPos, this._bufLength - this._bufPos);
    this._bufPos = this._bufLength;
    return data;
  }

  consumeLetterSequence(): string {
    this.bufferUp()
    let start = this._bufPos
    while (this._bufPos < this._bufLength) {
      let c = this._charBuf[this._bufPos]
      let cCode = c.charCodeAt(0)
      if ((cCode >= 'A'.charCodeAt(0) && cCode <= 'Z'.charCodeAt(0)) ||
      (cCode >= 'a'.charCodeAt(0) && cCode <= 'z'.charCodeAt(0)) || this.isLetter(c)) {
        this._bufPos++
      } else {
        break
      }
    }
    return CharacterReader.cacheString(this._charBuf, this._stringCache, start, this._bufPos - start);
  }

  consumeLetterThenDigitSequence(): string {
    this.bufferUp()
    let start = this._bufPos
    while (this._bufPos < this._bufLength) {
      let c = this._charBuf[this._bufPos]
      let cCode = c.charCodeAt(0)
      if ((cCode >= 'A'.charCodeAt(0) && cCode <= 'Z'.charCodeAt(0)) ||
      (cCode >= 'a'.charCodeAt(0) && cCode <= 'z'.charCodeAt(0)) || this.isLetter(c)) {
        this._bufPos++
      } else {
        break
      }
    }
    while (!this.isEmptyNoBufferUp()) {
      let cCode = this._charBuf[this._bufPos].charCodeAt(0)
      if (cCode >= '0'.charCodeAt(0) && cCode <= '9'.charCodeAt(0)) {
        this._bufPos++
      } else {
        break
      }
    }
    return CharacterReader.cacheString(this._charBuf, this._stringCache, start, this._bufPos - start);
  }

  consumeHexSequence(): string {
    this.bufferUp()
    let start = this._bufPos
    while (this._bufPos < this._bufLength) {
      let c = this._charBuf[this._bufPos]
      let cCode = c.charCodeAt(0)
      if ((cCode >= '0'.charCodeAt(0) && cCode <= '9'.charCodeAt(0)) ||
      (cCode >= 'a'.charCodeAt(0) && cCode <= 'f'.charCodeAt(0)) ||
      (cCode >= 'A'.charCodeAt(0) && cCode <= 'F'.charCodeAt(0))) {
        this._bufPos++
      } else {
        break
      }
    }
    return CharacterReader.cacheString(this._charBuf, this._stringCache, start, this._bufPos - start);
  }

  consumeDigitSequence(): string {
    this.bufferUp()
    let start = this._bufPos
    while (this._bufPos < this._bufLength) {
      let c = this._charBuf[this._bufPos]
      let cCode = c.charCodeAt(0)
      if ((cCode >= '0'.charCodeAt(0) && cCode <= '9'.charCodeAt(0))) {
        this._bufPos++
      } else {
        break
      }
    }
    return CharacterReader.cacheString(this._charBuf, this._stringCache, start, this._bufPos - start);
  }

  //java 中的 char c参数的
  matches(c: number | string): boolean {
    if (typeof c === 'number') {
      return!this.isEmpty() && this._charBuf[this._bufPos].charCodeAt(0) === c
    } else if (typeof c === 'string') {
      this.bufferUp()
      let scanLength = c.length
      if (scanLength > this._bufLength - this._bufPos) {
        return false
      }
      for (let offset = 0; offset < scanLength; offset++) {
        if (c[offset] !== this._charBuf[this._bufPos + offset]) {
          return false
        }
      }
      return true
    }
  }

  matchesIgnoreCase(seq: string) {
    this.bufferUp()
    let scanLength = seq.length
    if (scanLength > this._bufLength - this._bufPos) {
      return false
    }
    for (let offset = 0; offset < scanLength; offset++) {
      let upScan = seq[offset].toUpperCase()
      let upTarget = this._charBuf[this._bufPos+offset].toUpperCase()
      if (upScan !== upTarget) {
        hilog.info(0x0000, 'testTag', '%{public}s', `if upScan:${upScan}  upTarget:${upTarget}`);
        return false
      } else {
        hilog.info(0x0000, 'testTag', '%{public}s', `else upScan:${upScan}  upTarget:${upTarget}`);
      }
    }
    return true
  }

  matchesAny(...seq: string[]): boolean {
    if (this.isEmpty()) {
      return false
    }
    this.bufferUp()
    let c = this._charBuf[this._bufPos]
    for (let index = 0; index < seq.length; index++) {
      if (seq[index] === c) {
        return true
      }
    }
    return false
  }

  matchesAnySorted(seq: string[]): boolean {
    this.bufferUp()
    return!this.isEmpty() && ArrayUtils.binarySearch(seq, this._charBuf[this._bufPos]) >= 0
  }

  matchesLetter(): boolean {
    if (this.isEmpty()) {
      return false
    }
    let c = this._charBuf[this._bufPos]
    let cCode = c.charCodeAt(0)
    return (cCode >= 'A'.charCodeAt(0) && cCode <= 'Z'.charCodeAt(0)) ||
    (cCode >= 'a'.charCodeAt(0) && cCode <= 'z'.charCodeAt(0)) || this.isLetter(c)
  }

  /**
   Checks if the current pos matches an ascii alpha (A-Z a-z) per https://infra.spec.whatwg.org/#ascii-alpha
   @return if it matches or not
   */
  matchesAsciiAlpha(): boolean {
    if (this.isEmpty()) {
      return false
    }
    let c = this._charBuf[this._bufPos].charCodeAt(0)
    return (c >= 'A'.charCodeAt(0) && c <= 'Z'.charCodeAt(0)) || (c >= 'a'.charCodeAt(0) && c <= 'z'.charCodeAt(0))

  }

  matchesDigit(): boolean {
    if (this.isEmpty()) {
      return false
    }
    let cCode = this._charBuf[this._bufPos].charCodeAt(0)
    return (cCode >= '0'.charCodeAt(0) && cCode <= '9'.charCodeAt(0))
  }

  matchConsume(seq: string): boolean {
    this.bufferUp()
    if (this.matches(seq)) {
      this._bufPos += seq.length
      return true
    } else {
      return false
    }
  }

  matchConsumeIgnoreCase(seq: string): boolean {
    if (this.matchesIgnoreCase(seq)) {
      this._bufPos += seq.length
      return true
    } else {
      return false
    }
  }

  // we maintain a cache of the previously scanned sequence, and return that if applicable on repeated scans.
  // that improves the situation where there is a sequence of <p<p<p<p<p<p<p...</title> and we're bashing on the <p
  // looking for the </title>. Resets in bufferUp()
  private _lastIcSeq: string = '' // scan cache
  private _lastIcIndex: number = -1 // nearest found indexOf

  containsIgnoreCase(seq: string): boolean {
    if (seq === this._lastIcSeq) {
      if (this._lastIcIndex === -1) {
        hilog.info(0x0000, 'testTag', '%{public}s', `containsIgnoreCase:${-1}`);
        return false
      }
      if (this._lastIcIndex >= this._bufPos) {
        hilog.info(0x0000, 'testTag', '%{public}s', `containsIgnoreCase:${this._lastIcIndex} >= ${this._bufPos}`);
        return true
      }
    }
    this._lastIcSeq = seq
    hilog.info(0x0000, 'testTag', '%{public}s', `containsIgnoreCase:${this._lastIcSeq}`);
    let loScan = seq.toLowerCase()
    hilog.info(0x0000, 'testTag', '%{public}s', `containsIgnoreCase loScan:${loScan}`);
    let lo = this.nextIndexOf(loScan)
    hilog.info(0x0000, 'testTag', '%{public}s', `containsIgnoreCase:${loScan} :${lo}`);
    if (lo > -1) {
      this._lastIcIndex = this._bufPos + lo
      return true
    }
    let hiScan = seq.toUpperCase()
    let hi = this.nextIndexOf(hiScan)
    hilog.info(0x0000, 'testTag', '%{public}s', `containsIgnoreCase:${hiScan} :${hi}`);
    let found = hi > -1
    // we don't care about finding the nearest, just that buf contains
    this._lastIcIndex = found ? this._bufPos + hi : -1
    return found
  }

  toString(): string {
    if (this._bufLength - this._bufPos < 0) {
      return ""
    }
    return this._charBuf.slice(this._bufPos, this._bufLength).join('')
  }

  /**
   * Caches short strings, as a flywheel pattern, to reduce GC load. Just for this doc, to prevent leaks.
   * <p />
   * Simplistic, and on hash collisions just falls back to creating a new string, vs a full HashMap with Entry list.
   * That saves both having to create objects as hash keys, and running through the entry list, at the expense of
   * some more duplicates.
   */
  private static cacheString(charBuf: string[], stringCache: string[], start: number, count: number): string {
    // limit (no cache):
    if (count > CharacterReader._maxStringCacheLen) {
      return charBuf.slice(start, start + count).join('')
    }

    if (count < 1) {
      return ""
    }

    // calculate hash:
    let hash: number = 31 * count;
    let offset = start;
    for (let i = 0; i < count; i++) {
      hash = 31 * hash + charBuf[offset++].charCodeAt(0);
    }

    // get from cache
    let index = hash & CharacterReader._stringCacheSize - 1;
    let cached = stringCache[index];

    if (cached === undefined || cached === null) { // miss, add
      cached = charBuf.slice(start, start + count).join('')
      stringCache[index] = cached;
    } else { // hashcode hit, check equality
      if (CharacterReader.rangeEquals(charBuf, start, count, cached)) { // hit
        return cached;
      } else { // hashcode conflict
        cached = charBuf.slice(start, start + count).join('')
        stringCache[index] = cached; // update the cache, as recently used strings are more likely to show up again
      }
    }
    return cached;
  }

  /**
   * Check if the value of the provided range equals the string.
   */
  static rangeEquals(charBuf: string[], start: number, count: number, cached: string): boolean {
    if (count === cached.length) {
      let i = start;
      let j = 0;
      while (count-- != 0) {
        if (charBuf[i++] !== cached.charAt(j++))
          return false;
      }
      return true;
    }
    return false;
  }

  // just used for testing
  rangeEquals1(start: number, count: number, cached: string): boolean {
    return CharacterReader.rangeEquals(this._charBuf, start, count, cached)
  }


  isLetter(c: string): boolean {
    return /^[a-zA-Z]$/.test(c);
  }
}
